import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ClientWorker implements Runnable {
    /*
     * This class represents the work being done by threads for each client. A
     * thread running this job communicates with a single client. It contains
     * methods called act* that corresponds to whatever message the server
     * receives from the client. It also holds the static variable topic which
     * is shared among all clients.
     */
    private BufferedReader input;
    private String nickName = "";
    private final Socket clientSocket;
    private final String capaMsg = "* CAPABILITY VERSION=1.0 TOPIC WHOIS XPRIV XCHER\r\n";
    private final String welcomeMsg = "* OK Welcome to dv11elr & "
	    + "dv11rlm Leetspeak server.\r\n";

    private final Broadcaster broadcaster;
    private final Pattern regex = Pattern
	    .compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'");
    // This list holds all data on all connected clients on this chatserver.
    // it contains information about users not being handled by this specific
    // thread. This is useful for example to check that a nickname is not being
    // used in the chat by another client handled by another thread.
    private final ConcurrentHashMap<String, ChatServer.ClientsData> clientList;
    private static volatile String topic = "";

    public ClientWorker(Socket clientSocket, Broadcaster bcastJob,
	    ConcurrentHashMap<String, ChatServer.ClientsData> clientList) {

	this.clientSocket = clientSocket;

	broadcaster = bcastJob;
	this.clientList = clientList;

	try {
	    input = new BufferedReader(new InputStreamReader(
		    clientSocket.getInputStream()));
	} catch (Exception e) {
	    e.printStackTrace();
	}
    }

    @Override
    public void run() {

	try {
	    // send the untagged welcome message.
	    clientSocket.getOutputStream().write(welcomeMsg.getBytes("UTF-8"));

	    ArrayList<String> matchList = new ArrayList<String>();

	    while (!clientSocket.isClosed()) {

		String msg = input.readLine();
		matchList.clear();

		Matcher regexMatcher = regex.matcher(msg);
		while (regexMatcher.find()) {
		    matchList.add(regexMatcher.group());

		    if (matchList.size() == 2) {
			// We only need to isolate the 2 first strings
			// for most messages. The rest of the content is
			// dumped to index 3 and is processed later if
			// needed.
			matchList.add(msg.substring(regexMatcher.end()).trim());
			break;
		    }
		}
		String nTmp = matchList.get(1);

		if (nTmp.equals("TALK")) {
		    actTalk(matchList);
		} else if (nTmp.equals("NICK")) {
		    actNick(matchList);
		} else if (nTmp.equals("CAPABILITY")) {
		    actCapability(matchList);
		} else if (nTmp.equals("JOIN")) {
		    actJoin(matchList);
		} else if (nTmp.equals("LEAVE")) {
		    actLeave(matchList);
		} else if (nTmp.equals("QUIT")) {
		    actQuit(matchList);
		} else if (nTmp.equals("NAMES")) {
		    actNames(matchList);
		} else if (nTmp.equals("TOPIC")) {
		    actTopic(matchList);
		} else if (nTmp.equals("WHOIS")) {
		    actWhois(matchList);
		} else if (nTmp.equals("X_PRIV")) {
		    actPriv(matchList);
		} else if (matchList.get(0).equals("X_INIT")) {
		    // Filesharing protocol does not use tags so element 0
		    // has the command.
		    actXcher(matchList, msg);
		} else {
		    String badMsg = matchList.get(0)
			    + " BAD Unknown command.\r\n";
		    clientSocket.getOutputStream().write(
			    badMsg.getBytes("UTF-8"));
		}

	    }

	} catch (Exception e) {
	    // Socket likely died. Exiting thread gracefully.
	    try {
		// if client did a hard disconnect the data is still present and
		// must be removed.
		if (clientList.containsKey(nickName)) {
		    clientList.remove(nickName);
		    String name = nickName;

		    if (name.contains(" ")) {
			name = "\"" + name + "\"";
		    }
		    String quitMsg = "* LEAVE " + name + "\r\n";
		    // broadcast the LEAVE to everyone in chat.
		    broadcaster.addMsg(quitMsg);
		    clientSocket.close();
		}
	    } catch (Exception e1) {
		// Failed to exit gracefully.
	    }
	}

    }

    private void actNick(ArrayList<String> matchList) throws IOException {
	/* actNick this method handles a NICK command from the client. */

	// If no arguments received, send back untagged message with current
	// nickname.
	if (matchList.get(2).isEmpty()) {
	    String name = nickName;
	    if (name.contains(" ")) {
		name = "\"" + name + "\"";
	    }
	    String nickMsg = "* NICK " + name + "\r\n";
	    clientSocket.getOutputStream().write(nickMsg.getBytes("UTF-8"));
	    return;
	}
	// remove any "" surrounding the nick.
	String n_tmp = matchList.get(2).replace("\"", "");

	// if the requested nickname is already in use we can stop here.
	if (clientList.containsKey(n_tmp)) {

	    String nickMsg = matchList.get(0)
		    + " NO Name is already in use.\r\n";
	    clientSocket.getOutputStream().write(nickMsg.getBytes("UTF-8"));
	    return;
	}

	if (nickName.isEmpty()) {
	    // Assign a nickname to this client.
	    nickName = n_tmp;
	    String nickMsg = "* NICK " + nickName + "\r\n";
	    clientSocket.getOutputStream().write(nickMsg.getBytes("UTF-8"));
	    nickMsg = matchList.get(0) + " OK NICK completed.\r\n";
	    clientSocket.getOutputStream().write(nickMsg.getBytes("UTF-8"));
	    clientList.put(nickName, new ChatServer.ClientsData(clientSocket,
		    nickName, false));
	} else {
	    // A rename was requested.
	    ChatServer.ClientsData oldData = clientList.get(nickName);
	    String oldNick = nickName;
	    nickName = n_tmp;
	    ChatServer.ClientsData newData = new ChatServer.ClientsData(
		    clientSocket, nickName, oldData.isActive());
	    // remove old client data.
	    clientList.remove(oldNick);
	    // add new client data.
	    clientList.put(nickName, newData);

	    // wrap the nicknames with "" if any of them contain a space.
	    // the nicknames are only wrapped for RENAME message.
	    if (nickName.contains(" ")) {
		n_tmp = "\"" + n_tmp + "\"";
	    }
	    if (oldNick.contains(" ")) {
		oldNick = "\"" + oldNick + "\"";
	    }

	    String nickMsg = "* RENAME " + oldNick + " " + n_tmp + "\r\n";
	    // broadcast the rename to everyone in chat.
	    if (clientList.get(nickName).isActive()) {
		broadcaster.addMsg(nickMsg);
	    }

	    nickMsg = matchList.get(0) + " OK NICK completed.\r\n";
	    clientSocket.getOutputStream().write(nickMsg.getBytes("UTF-8"));
	}
    }

    private void actCapability(ArrayList<String> matchList) throws IOException {
	/*
	 * actCapability this method responds to the request made by the user to
	 * see what kind of protocol and plugins the server supports.
	 */
	clientSocket.getOutputStream().write(capaMsg.getBytes("UTF-8"));
	clientSocket.getOutputStream().write(
		(matchList.get(0) + " OK CAPABILITY completed.\r\n")
			.getBytes("UTF-8"));
    }

    private void actJoin(ArrayList<String> matchList) throws IOException {
	/*
	 * actJoin runs when a client requested to join the chat channel. Some
	 * requirements are checked before marking the user as actively chatting
	 * and letting him join the chat.
	 */
	String joinMsg;

	// if nickName isn't set, JOIN is not allowed.
	if (nickName.isEmpty()) {
	    joinMsg = matchList.get(0) + " NO You have no name.\r\n";
	    clientSocket.getOutputStream().write(joinMsg.getBytes("UTF-8"));
	    return;
	}
	ChatServer.ClientsData cD = clientList.get(nickName);
	// Ignore JOIN if already chatting.
	if (cD != null && cD.isActive()) {
	    joinMsg = matchList.get(0) + " NO You're already chatting.\r\n";
	    clientSocket.getOutputStream().write(joinMsg.getBytes("UTF-8"));
	    return;
	}
	// When connected this client is marked as active. This value is
	// used by the broadcaster to ignore clients not participating in the
	// chat.
	clientList.get(nickName).setActive(true);
	String name = nickName;

	if (name.contains(" ")) {
	    name = "\"" + name + "\"";
	}
	joinMsg = "* JOIN " + name + "\r\n";
	broadcaster.addMsg(joinMsg);

	joinMsg = matchList.get(0) + " OK JOIN completed.\r\n";
	clientSocket.getOutputStream().write(joinMsg.getBytes("UTF-8"));

	// Send out TOPIC to the newly joined client.
	String topicMsg = "* TOPIC " + topic + "\r\n";
	clientSocket.getOutputStream().write(topicMsg.getBytes("UTF-8"));

    }

    private void actNames(ArrayList<String> matchList) throws IOException {
	/*
	 * actNames runs when a client requested to get the name of all actively
	 * chatting clients on the chatserver. Each username is sent back in a
	 * Separate response to the requester.
	 */
	String namesMsg;

	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}

	for (ChatServer.ClientsData cData : clientList.values()) {
	    // for each active client in chat send their nicks to this client.
	    if (cData.isActive()) {
		String name = cData.getNickname();
		// pad with "" only on space existence.
		if (name.contains(" ")) {
		    name = "\"" + name + "\"";
		}
		namesMsg = "* NAMES " + name + "\r\n";
		clientSocket.getOutputStream()
			.write(namesMsg.getBytes("UTF-8"));
	    }
	}
	namesMsg = matchList.get(0) + " OK NAMES completed.\r\n";
	clientSocket.getOutputStream().write(namesMsg.getBytes("UTF-8"));
    }

    private void actLeave(ArrayList<String> matchList) throws IOException {
	/*
	 * actLeave runs when a client requested to get leave the chat. This is
	 * not allowed if the user isn't marked as actively chatting. Basically
	 * what it does is just mark the requester as not actively chatting and
	 * then tell the broadcaster to announce that the client left the chat.
	 */

	// refuse if client is not chatting.
	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}

	// remove client data.
	clientList.get(nickName).setActive(false);
	String name = nickName;

	if (name.contains(" ")) {
	    name = "\"" + name + "\"";
	}
	String leaveMsg = "* LEAVE " + name + "\r\n";
	// broadcast the LEAVE to everyone in chat.
	broadcaster.addMsg(leaveMsg);

	leaveMsg = matchList.get(0) + " OK LEAVE completed.\r\n";
	clientSocket.getOutputStream().write(leaveMsg.getBytes("UTF-8"));
    }

    private void actQuit(ArrayList<String> matchList) throws IOException {
	/*
	 * actQuit runs when a client requested to exit the chatserver. The
	 * difference between a leave and a quit is that quit completely removes
	 * the requester from the chatserver and closes any thread/socket that
	 * is handling him.
	 */

	// remove client data.
	clientList.remove(nickName);
	String name = nickName;

	if (name.contains(" ")) {
	    name = "\"" + name + "\"";
	}
	String quitMsg = "* BYE " + name + " quits.\r\n";
	clientSocket.getOutputStream().write(quitMsg.getBytes("UTF-8"));

	quitMsg = "* LEAVE " + name + "\r\n";
	// broadcast the LEAVE to everyone in chat.
	broadcaster.addMsg(quitMsg);

	quitMsg = matchList.get(0) + " OK QUIT completed.\r\n";
	clientSocket.getOutputStream().write(quitMsg.getBytes("UTF-8"));
	clientSocket.close();
    }

    private void actTalk(ArrayList<String> matchList) throws IOException {
	/*
	 * actTalk runs when a client request to speak to the rest of the
	 * clients in the chat this is a regular text string that is passed to
	 * the broadcaster to be sent to all actively chatting clients on the
	 * chatserver.
	 */

	// refuse if client is not chatting.
	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}

	String n_tmp = nickName;
	// wrap the nickname with "" if it contains a space.
	if (nickName.contains(" ")) {
	    n_tmp = "\"" + nickName + "\"";
	}

	String talkMsg = "* TALK " + n_tmp + " " + matchList.get(2) + "\r\n";
	// broadcast the TALK to everyone in chat.
	broadcaster.addMsg(talkMsg);
	talkMsg = matchList.get(0) + " OK TALK completed.\r\n";
	clientSocket.getOutputStream().write(talkMsg.getBytes("UTF-8"));
    }

    private void actTopic(ArrayList<String> matchList) throws IOException {
	/*
	 * actTopic runs when a client request the current topic or when he
	 * wants to set it. This is based on the number of arguments supplied.
	 */

	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}
	// If no arguments received, send back untagged message with current
	// topic.
	if (matchList.get(2).isEmpty()) {
	    String topicMsg = "* TOPIC " + topic + "\r\n";
	    clientSocket.getOutputStream().write(topicMsg.getBytes("UTF-8"));
	} else {
	    // a topic was received.
	    topic = matchList.get(2);
	    String topicMsg = "* TOPIC " + topic + "\r\n";
	    broadcaster.addMsg(topicMsg);
	    topicMsg = matchList.get(0) + " OK TOPIC completed.\r\n";
	    clientSocket.getOutputStream().write(topicMsg.getBytes("UTF-8"));
	}

    }

    private void actWhois(ArrayList<String> matchList) throws IOException {
	/*
	 * actWhois runs when a client request to get WHOIS information on a
	 * specific client. It looks up the supplied nickname in its list and
	 * retrieves connected ip+port for that nick.
	 */
	String whoisMsg;
	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}

	ChatServer.ClientsData uData = clientList.get(matchList.get(2));

	if (uData != null && uData.isActive()) {
	    whoisMsg = "* WHOIS " + matchList.get(2) + " "
		    + uData.getSocket().getInetAddress().getHostAddress() + " "
		    + uData.getSocket().getPort() + " \r\n";
	    clientSocket.getOutputStream().write(whoisMsg.getBytes("UTF-8"));

	    whoisMsg = matchList.get(0) + " OK WHOIS completed.\r\n";
	    clientSocket.getOutputStream().write(whoisMsg.getBytes("UTF-8"));
	} else {
	    whoisMsg = matchList.get(0)
		    + " NO whois target isn't chatting.\r\n";
	    clientSocket.getOutputStream().write(whoisMsg.getBytes("UTF-8"));
	}

    }

    private void actPriv(ArrayList<String> matchList) throws IOException {
	/*
	 * actPriv runs when a client request to get WHOIS information on a
	 * specific client. It looks up the supplied nickname in its list and
	 * retrieves connected ip+port for that nick.
	 */

	// Added feature. To use this function server expects a command
	// tag X_PRIV "clientname". This will send a private message to
	// "clientname".
	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}

	String n_tmp = nickName;
	// wrap the nickname with "" if it contains a space.
	if (nickName.contains(" ")) {
	    n_tmp = "\"" + nickName + "\"";
	}

	// isolate the target nickname+message
	Matcher regexMatcher = regex.matcher(matchList.get(2));
	if (regexMatcher.find()) {
	    matchList.add(regexMatcher.group());
	    matchList
		    .add(matchList.get(2).substring(regexMatcher.end()).trim());
	}

	if (matchList.size() < 4) {
	    String privMsg = matchList.get(0) + " BAD missing argument.\r\n";
	    clientSocket.getOutputStream().write(privMsg.getBytes("UTF-8"));
	    return;
	}

	ChatServer.ClientsData cD = clientList.get(matchList.get(3).replace(
		"\"", ""));
	// if target client exist and is actively chatting.
	if (cD != null && cD.isActive()) {
	    String privMsg = "* X_PRIV " + n_tmp + " " + matchList.get(4)
		    + "\r\n";
	    cD.getSocket().getOutputStream().write(privMsg.getBytes("UTF-8"));

	    privMsg = matchList.get(0) + " OK X_PRIV completed.\r\n";
	    clientSocket.getOutputStream().write(privMsg.getBytes("UTF-8"));
	} else {
	    String privMsg = matchList.get(0)
		    + " NO X_PRIV target could not be messaged.\r\n";
	    clientSocket.getOutputStream().write(privMsg.getBytes("UTF-8"));
	}
    }

    private void actXcher(ArrayList<String> matchList, String origMsg)
	    throws IOException {
	/*
	 * actXcher runs when a client request to setup an XCHER connection to
	 * supplied nick. This method just extracts the nickname, finds the
	 * socket the client with that nickname is connected to. Then redirects
	 * the information towards that specific client.
	 */

	if (notChattingBlocked(matchList.get(0))) {
	    return;
	}

	ChatServer.ClientsData tData = clientList.get(matchList.get(1).replace(
		"\"", ""));

	// check that given target exists and is actively chatting. then
	// redirect this message to the target.
	if (tData != null && tData.isActive()) {
	    String xcherMsg = origMsg + " \r\n";
	    tData.getSocket().getOutputStream()
		    .write(xcherMsg.getBytes("UTF-8"));

	    // note no confirmation is sent back, the sender should timeout if
	    // no connection is made to his socket.
	}

    }

    private boolean notChattingBlocked(String tag) throws IOException {
	// This check is done for commands that are disabled if the client is
	// not participating in the chat.
	ChatServer.ClientsData cD = clientList.get(nickName);

	if (cD == null || cD.isActive() == false) {
	    String denyMsg = tag + " NO You're not chatting.\r\n";
	    clientSocket.getOutputStream().write(denyMsg.getBytes("UTF-8"));
	    return true;
	}
	return false;
    }
}
